In [1]:
from collections import Counter
from operator import itemgetter, attrgetter
from functools import reduce, partial

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.figure_factory as ff
import plotly.offline as pyo
from mongoengine import connect

import src.settings as settings
from src.visualization.map import Point, Area
from src.visualization.statistics import *
from src.features.preprocessing import convert_salary
from src.data.vacancy import Vacancy
In [2]:
connect(
    host=settings.DB_HOST,
    port=settings.DB_PORT,
    db=settings.DB_NAME
)
Out[2]:
MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True, read_preference=Primary())
In [3]:
pyo.init_notebook_mode()
In [4]:
%load_ext autoreload
%autoreload 2

Получение данных¶

In [5]:
df: pd.DataFrame = (
    Vacancy
        .objects
        .to_dataframe(include=[
        '_id',
        'name',
        'description',
        'salary',
        'schedule.name',
        'experience',
        'employment.name',
        'area.name',
        'address.lat',
        'address.lng',
        'address.city',
        'specializations',
        'employer.name',
        'professional_roles',
        'key_skills',
    ])
)
In [6]:
df.set_index('_id', inplace=True)

Обработка¶

Удаление вакансий без зарплаты¶

In [7]:
df['salary.to'].fillna(df['salary.from'], inplace=True)

df = df[df['salary.from'].notna()]
df = df[df['salary.to'].notna()]
df = df[df['salary.currency'].notna()]
In [8]:
df['salary.currency'].isna().sum()
Out[8]:
0
In [9]:
df.shape
Out[9]:
(47440, 18)

Перевод всех зарплат в рубли¶

In [10]:
df[['salary.from', 'salary.to', 'salary.currency']] = df[['salary.from', 'salary.to', 'salary.currency']].apply(
    lambda row: [
        convert_salary(row['salary.from'], from_currency=row['salary.currency'], db=settings.db),
        convert_salary(row['salary.to'], from_currency=row['salary.currency'], db=settings.db),
        row['salary.currency']
    ], axis=1, result_type='expand')
In [11]:
df['mean_salary'] = np.round((df['salary.to'] + df['salary.from']) / 2)
In [12]:
df[df['salary.currency'] != 'RUR'].head(10)
Out[12]:
description key_skills schedule.name experience.id experience.name employment.name salary.to salary.from salary.currency salary.gross name area.name employer.name specializations professional_roles address.city address.lat address.lng mean_salary
_id
49810443 Требуемый опыт работы: 1–3 года Частичная заня... [Internet, Голландский язык, Работа в команде,... Удаленная работа between1And3 От 1 года до 3 лет Частичная занятость 84674.005080 16934.801016 EUR True Преподаватель голландского языка (онлайн) Алматы Lingolands [{'id': '14.60', 'name': 'Гуманитарные науки',... [{'id': '132', 'name': 'Учитель, преподаватель... NaN NaN NaN 50804.0
49226918 Сеть магазинов "Соседи" приглашает на работу з... [Управление персоналом, Пользователь ПК, Работ... Сменный график between1And3 От 1 года до 3 лет Полная занятость 37745.710055 25550.942191 BYR False Заведующий отделом розничных продаж Смолевичи СОСЕДИ, Сеть магазинов [{'id': '17.324', 'name': 'Управление продажам... [{'id': '127', 'name': 'Товаровед'}] NaN NaN NaN 31648.0
49225403 Обязанности: ручная бережная и качественная м... [] Полный день noExperience Нет опыта Полная занятость 29035.161581 29035.161581 BYR False Мойщик автомобилей Минск Такси Алмаз 7788 [{'id': '15.390', 'name': 'Автомобильный бизне... [{'id': '4', 'name': 'Автомойщик'}] Минск 53.899117 27.524919 29035.0
49225631 Приглашаем водителей для работы в "Такси Алмаз... [] Гибкий график noExperience Нет опыта Частичная занятость 43552.742371 43552.742371 BYR False Водитель Такси Алмаз 7788 Минск Такси Алмаз 7788 [{'id': '21.482', 'name': 'Водитель', 'profare... [{'id': '21', 'name': 'Водитель'}] Минск 53.899117 27.524919 43553.0
50155391 Обязанности: - Обработка входящего потока сооб... [Грамотная речь, Пользователь ПК, Работа в ком... Удаленная работа noExperience Нет опыта Полная занятость 44716.053063 25339.096736 USD False Менеджер по продажам в мессенджерах (Direct-ме... Киев Миронов Владимир [{'id': '17.149', 'name': 'Менеджер по работе ... [{'id': '54', 'name': 'Координатор отдела прод... NaN NaN NaN 35028.0
50157216 Please note that by applying to this vacancy y... [Английский язык, Coaching, Leadership Skills,... Полный день noExperience Нет опыта Стажировка 63405.792445 63405.792445 KZT True Sales Intern/ Стажер в отдел продаж Алматы Procter & Gamble [{'id': '15.389', 'name': 'Продажи', 'profarea... [{'id': '40', 'name': 'Другое'}] NaN NaN NaN 63406.0
50155707 Обязанности: Запуск рекламных компаний Google,... [Английский язык, Маркетинговый анализ, Подгот... Полный день between1And3 От 1 года до 3 лет Полная занятость 260843.642868 111790.132658 USD False PPC specialist / Специалист по контекстной рек... Москва WeFix Appliance Repair [{'id': '1.246', 'name': 'Развитие бизнеса', '... [{'id': '68', 'name': 'Менеджер по маркетингу ... NaN NaN NaN 186317.0
50156232 Makeomatic расширяет свою команду! Уже 7 лет м... [Git, JavaScript, Node.js, Docker, GitHub, Dev... Полный день between3And6 От 3 до 6 лет Полная занятость 335370.397973 260843.642868 USD True Senior Backend Developer (Node.js) Москва Makeomatic Inc [{'id': '1.221', 'name': 'Программирование, Ра... [{'id': '96', 'name': 'Программист, разработчи... NaN NaN NaN 298107.0
50157118 Топ-менеджер/Руководитель/Управление (банковск... [Работа в команде, CRM, Телефонные переговоры,... Удаленная работа between1And3 От 1 года до 3 лет Полная занятость 335370.397973 111790.132658 USD False Руководитель (Управление, банковская сфера) Владивосток Красный Джин [{'id': '9.226', 'name': 'Продажи', 'profarea_... [{'id': '40', 'name': 'Другое'}] NaN NaN NaN 223580.0
50157432 Julia Valler Event Staffing is a high-end mode... [Английский язык, Работа в команде, MS PowerPo... Полный день noExperience Нет опыта Полная занятость 74526.755105 74526.755105 USD True Sales Manager (Full Time Remote) США Julia Valler Staffing [{'id': '17.242', 'name': 'Прямые продажи', 'p... [{'id': '70', 'name': 'Менеджер по продажам, м... NaN NaN NaN 74527.0
In [13]:
df.head(10)
Out[13]:
description key_skills schedule.name experience.id experience.name employment.name salary.to salary.from salary.currency salary.gross name area.name employer.name specializations professional_roles address.city address.lat address.lng mean_salary
_id
49810439 Обязанности: Своевременная подача автомобиля; ... [] Полный день between3And6 От 3 до 6 лет Полная занятость 80000.0 60000.0 RUR False Водитель в семью Москва Антонова Анастасия [{'id': '21.17', 'name': 'Автоперевозки', 'pro... [{'id': '21', 'name': 'Водитель'}] NaN NaN NaN 70000.0
49810551 Обязанности: Уборка дома 500 кв.м., стирка, г... [Русский язык, Чистоплотность] Полный день between1And3 От 1 года до 3 лет Полная занятость 105000.0 100000.0 RUR False Помощник по хозяйству на дачу Санкт-Петербург Агентство Прайм Домашний Персонал [{'id': '4.494', 'name': 'Уборщица/уборщик', '... [{'id': '130', 'name': 'Уборщица, уборщик'}] Санкт-Петербург 59.932428 30.439198 102500.0
49810468 Студия Красоты и здоровья Кристалл ищет парикм... [Пользователь ПК, Работа в команде, Грамотная ... Полный день between1And3 От 1 года до 3 лет Полная занятость 25000.0 25000.0 RUR True Парикмахер-универсал Волгоград Кристалл [{'id': '24.493', 'name': 'Парикмахер', 'profa... [{'id': '92', 'name': 'Парикмахер'}] NaN NaN NaN 25000.0
45788942 Условия: ЗП от 50 тысяч на руки (оклад 22 тыся... [Складская логистика, Терминалы Сбора Данных, ... Сменный график noExperience Нет опыта Полная занятость 80000.0 50000.0 RUR False Кладовщик - комплектовщик Тула Симпл Деливери Груп [{'id': '21.563', 'name': 'Кладовщик', 'profar... [{'id': '131', 'name': 'Упаковщик, комплектовщ... рабочий посёлок Горки Ленинские 55.520630 37.774149 65000.0
49810601 Уважаемые соискатели, рассматриваются кандидат... [] Полный день moreThan6 Более 6 лет Полная занятость 100000.0 100000.0 RUR False Заместитель главного бухгалтера (производство) Ростов-на-Дону АнРуссТранс [{'id': '2.335', 'name': 'Учет заработной плат... [{'id': '18', 'name': 'Бухгалтер'}] NaN NaN NaN 100000.0
49810507 Логопедический Пункт 1 приглашает Администрато... [Обучение персонала, Пользователь ПК, Организа... Полный день between1And3 От 1 года до 3 лет Полная занятость 30000.0 25000.0 RUR True Администратор детского центра Волгоград Логопедический Пункт №1 [{'id': '4.332', 'name': 'Управляющий офисом (... [{'id': '8', 'name': 'Администратор'}] Волгоград 48.745092 44.499916 27500.0
49810469 Студия Красоты и здоровья Кристалл ищет парикм... [Пользователь ПК, Работа в команде, Грамотная ... Полный день between1And3 От 1 года до 3 лет Полная занятость 25000.0 25000.0 RUR True Парикмахер-универсал Волжский (Волгоградская область) Кристалл [{'id': '24.493', 'name': 'Парикмахер', 'profa... [{'id': '92', 'name': 'Парикмахер'}] NaN NaN NaN 25000.0
49810426 Обязанности: выполнение услуг массажа на высок... [антицеллюлитный, класический, спортивный, лим... Полный день between3And6 От 3 до 6 лет Полная занятость 150000.0 80000.0 RUR True Массажистка/массажист Москва ЭК Брендинг [{'id': '24.492', 'name': 'Массажист', 'profar... [{'id': '60', 'name': 'Массажист'}] Москва 55.778796 37.598825 115000.0
47003369 Медиахолдинг "Май Медиа" ищет менеджера по про... [Прямые продажи, Телефонные переговоры, Навыки... Полный день between1And3 От 1 года до 3 лет Полная занятость 70000.0 50000.0 RUR False Ведущий клиентский менеджер Иваново (Ивановская область) Май Медиа [{'id': '17.242', 'name': 'Прямые продажи', 'p... [{'id': '105', 'name': 'Руководитель отдела кл... Иваново 57.001064 40.968217 60000.0
43592367 Обязанности: Запрос цен и анализ по счетам от... [MS PowerPoint, MS Access, Работа с базами дан... Сменный график noExperience Нет опыта Полная занятость 31500.0 26250.0 RUR True Оператор базы данных Белгород My Sky [{'id': '2.33', 'name': 'Аудит', 'profarea_id'... [{'id': '84', 'name': 'Оператор ПК, оператор б... Белгород 50.576507 36.578904 28875.0

Добавление средней зарплаты¶

In [14]:
total_salary = df['mean_salary'].sum()
total_salary
Out[14]:
2782463002.0
In [15]:
total_salary_by_area = df[['area.name', 'mean_salary']].groupby(['area.name'], as_index=False).sum().rename(
    columns={'mean_salary': 'total_salary'})
total_salary_by_area.head(10)
Out[15]:
area.name total_salary
0 Абаза 770000.0
1 Абай 86000.0
2 Абакан 8340594.0
3 Абан 119921.0
4 Абатское 195048.0
5 Абинск 65000.0
6 Авсюнино 131500.0
7 Агалатово 30000.0
8 Агаповка 86000.0
9 Агеево 66000.0

Анализ¶

Разделение зарплат по городам¶

In [16]:
other = total_salary_by_area.total_salary < (total_salary / 100)  # общая зарплата меньше 1%
other_value = total_salary_by_area.total_salary[other].agg('sum')
total_salary_by_area = total_salary_by_area[~other]
total_salary_by_area = total_salary_by_area.append({'area.name': 'Другие регионы', 'total_salary': other_value},
                                                   ignore_index=True)
In [17]:
total_salary_by_area
Out[17]:
area.name total_salary
0 Владивосток 6.015154e+07
1 Екатеринбург 4.013486e+07
2 Иркутск 4.925897e+07
3 Казань 4.131740e+07
4 Краснодар 5.243045e+07
5 Красноярск 5.313064e+07
6 Минск 2.859788e+07
7 Москва 5.370685e+08
8 Нижний Новгород 3.684292e+07
9 Новосибирск 6.767413e+07
10 Ростов-на-Дону 3.439070e+07
11 Санкт-Петербург 2.051561e+08
12 Уфа 2.819120e+07
13 Хабаровск 4.851375e+07
14 Другие регионы 1.499604e+09
In [18]:
px.pie(total_salary_by_area, names='area.name', values='total_salary', title='Разделение зарплат по городам')

Количество вакансий и средняя зарплата относительно местоположения¶

In [19]:
geo_df = df.reset_index()[['_id', 'address.city', 'address.lat', 'address.lng', 'mean_salary']]\
    .groupby('address.city', as_index=False)\
    .agg({'mean_salary': 'mean', '_id': 'count', 'address.lat': 'mean', 'address.lng': 'mean'})\
    .rename(columns={'_id': 'count'})
In [20]:
px.scatter_geo(
    geo_df[(geo_df['count'] > 10) & (geo_df['mean_salary'] < 200_000)],
    lat='address.lat',
    lon='address.lng',
    size='count',
    fitbounds='locations',
    color='mean_salary',
    hover_data=['address.city'],
    center={'lat': 53, 'lon': 83},
    size_max=50,
    labels={'mean_salary': 'Зарплата'},
    title='Количество вакансий и средняя зарплата относительно города'
)

Распределения¶

In [21]:
px.histogram(
    df[df.mean_salary < 500_000],
    x='mean_salary',
    nbins=100,
    title='Распределение зарплат',
    labels={'mean_salary': 'Зарплата'}
)
In [22]:
px.histogram(
    df[df.mean_salary < 500_000],
    x='mean_salary',
    color='schedule.name',
    nbins=100,
    title='Распределение зарплат c учетом графика работы',
    labels={'mean_salary': 'Зарплата', 'schedule.name': 'График'}
)
In [23]:
px.histogram(
    df[df.mean_salary < 500_000],
    x='mean_salary',
    color='salary.currency',
    nbins=100,
    title='Распределение зарплат c учетом валюты работы',
    labels={'mean_salary': 'Зарплата', 'salary.currency': 'Валюта'}
)
In [24]:
px.histogram(
    df,
    x='salary.currency',
    title='Количество вакансий для каждой валюты',
    labels={'salary.currency': 'Валюта'}
).update_xaxes(categoryorder='total descending')

Анализ зарплат относительно навыков¶

In [25]:
key_skills = reduce(set.union, df.key_skills, set())
In [26]:
len(key_skills)
Out[26]:
7364
In [27]:
key_skill_df = pd.DataFrame((
    {
        'Навык': key_skill,
        **df[df['key_skills'].map({key_skill}.issubset)]['mean_salary']
            .agg(['count', 'sum', 'mean', 'max', 'min']).to_dict()
    }
    for key_skill in key_skills
))
In [28]:
ff.create_table(key_skill_df[key_skill_df['Навык'].map(len) < 30].head(50).astype(int, errors='ignore'))
In [29]:
px.pie(
    key_skill_df[key_skill_df['count'] > 1_000],
    names='Навык',
    values='count',
    title='Доля вакансий для самых популярных навыков',
    labels={'count': 'Количество вакансий'}
)
In [30]:
sorted_key_skill_df = key_skill_df.sort_values('mean', ascending=False)
In [31]:
px.bar(
    sorted_key_skill_df.head(20),
    x='Навык',
    y='mean',
    color='count',
    title='Средняя зарплата самых высокооплачиваемых навыков',
    labels={'mean': 'Средняя зарплата', 'count': 'Количество вакансий'},
    range_color=(1, 3),
    text_auto='.2s'
).update_xaxes(categoryorder='total descending')
In [32]:
px.bar(
    sorted_key_skill_df[sorted_key_skill_df['count'] >= df.shape[0] * 0.008 * 0.01].head(20),
    x='Навык',
    y='mean',
    color='count',
    title='Средняя зарплата самых высокооплачиваемых навыков, необходимых не менее чем в 0.008% вакансий',
    labels={'mean': 'Средняя зарплата', 'count': 'Количество вакансий'},
    range_color=(1, 30),
    text_auto='.2s',
    height=700,
).update_xaxes(categoryorder='total descending')
In [33]:
px.bar(
    sorted_key_skill_df[sorted_key_skill_df['count'] >= df.shape[0] * 0.1 * 0.01].head(20),
    x='Навык',
    y='mean',
    color='count',
    title='Средняя зарплата самых высокооплачиваемых навыков, необходимых не менее чем в 0.1% вакансий',
    labels={'mean': 'Средняя зарплата', 'count': 'Количество вакансий'},
    range_color=(1, 300),
    text_auto='.2s'
).update_xaxes(categoryorder='total descending')

Анализ зарплат относительно профобластей¶

In [34]:
specs_df = df.copy()

specs_df.specializations = specs_df.specializations.map(lambda specs: list(map(itemgetter('name'), specs)))
specs_df = specs_df[specs_df.specializations.notna()]

specs_df['specialization_profarea_names'] = df.specializations.map(
    lambda specs: list(set(map(itemgetter('profarea_name'), specs))))
specs_df = specs_df[specs_df.specialization_profarea_names.notna()]

specs_df['full_specializations'] = df.specializations
In [35]:
specs_df[['specialization_profarea_names', 'specializations']].head(10)
Out[35]:
specialization_profarea_names specializations
_id
49810439 [Транспорт, логистика] [Автоперевозки, Водитель, Логистика, Экспедитор]
49810551 [Административный персонал, Домашний персонал] [Уборщица/уборщик, домработница/домработник, Г...
49810468 [Спортивные клубы, фитнес, салоны красоты] [Парикмахер]
45788942 [Транспорт, логистика] [Кладовщик, Рабочий склада, Логистика]
49810601 [Банки, инвестиции, лизинг, Бухгалтерия, управ... [Учет заработной платы, Основные средства, Нал...
49810507 [Административный персонал] [Управляющий офисом (Оffice manager), Персонал...
49810469 [Спортивные клубы, фитнес, салоны красоты] [Парикмахер]
49810426 [Спортивные клубы, фитнес, салоны красоты] [Массажист]
47003369 [Продажи] [Прямые продажи, Менеджер по работе с клиентами]
43592367 [Бухгалтерия, управленческий учет, финансы пре... [Аудит, Другое, Финансовый анализ]
In [36]:
all_specialization_names = list(reduce(set.union, specs_df.specializations, set()))
all_specialization_names[:15]
Out[36]:
['Учет заработной платы',
 'Системная интеграция',
 'Шлифовщик',
 'Основные средства',
 'Корпоративное право',
 'Охранник',
 'Ценные бумаги',
 'Авторское право',
 'Журналистика',
 'Фармацевтическая промышленность',
 'Фотография',
 'Интернет',
 'Кассир, Инкассатор',
 'Отопление, вентиляция и кондиционирование',
 'Таможенное оформление']
In [37]:
len(all_specialization_names)
Out[37]:
504
In [38]:
profarea_by_specialization = reduce(
    dict.__or__,
    map(
        lambda specs:
        dict(zip(
            map(itemgetter('name'), specs),
            map(itemgetter('profarea_name'), specs)
        )),
        specs_df.full_specializations
    )
)
In [39]:
ff.create_table([('Специализация', 'Профобласть')] + list(profarea_by_specialization.items())[:50])
In [40]:
count_by_specialization = {spec: specs_df.specializations.map({spec}.issubset).sum() for spec in all_specialization_names}
count_by_specialization = Counter(count_by_specialization)
In [41]:
ff.create_table([('Специализация', 'Количество вакансий')] + count_by_specialization.most_common(10))
In [42]:
spec_names_df = pd.DataFrame((
    {
        'Специализация': spec,
        'Профобласть': profarea_by_specialization[spec],
        **specs_df[specs_df.specializations.map({spec}.issubset)]['mean_salary']
            .agg(['count', 'sum', 'mean', 'max', 'min']).to_dict()
    }
    for spec in all_specialization_names
))
In [43]:
ff.create_table(spec_names_df[(spec_names_df['Специализация'].map(len) < 20) & (spec_names_df['Профобласть'].map(len) < 20)].head(50))
In [44]:
px.bar(
    spec_names_df.sort_values('mean', ascending=False).head(30),
    x='Специализация',
    y='mean',
    color='Профобласть',
    labels={'mean': 'Средняя зарплата'},
    title='30 самых высокооплачиваемых специализаций',
    text_auto='.2s'
).update_xaxes(categoryorder='total descending')
In [45]:
geo_profareas_df = pd.concat(map(
    pd.DataFrame,
    (dict(zip(
        ('profarea', 'lat', 'lng'),
        zip(*zip(
            spec_names := set(map(itemgetter('profarea_name'), specs)),
            [lat] * len(spec_names),
            [lng] * len(spec_names)
        ))
    ))
    for lat, lng, specs in df[['address.lat', 'address.lng', 'specializations']].dropna().values)
)).reset_index().drop('index', axis=1)
In [46]:
ff.create_table(geo_profareas_df.head(15))
In [47]:
# noinspection PyTypeChecker
points = reduce(list.__add__, [
    list(map(Point.from_tuple, zip(
        spec_names := set(map(itemgetter('profarea_name'), specs)),
        [lat] * len(spec_names),
        [lng] * len(spec_names)
    )))
    for lat, lng, specs in df[['address.lat', 'address.lng', 'specializations']].dropna().values
])
In [48]:
points[:100]
Out[48]:
[Point(profarea='Административный персонал', lat=59.932428, lng=30.439198),
 Point(profarea='Домашний персонал', lat=59.932428, lng=30.439198),
 Point(profarea='Транспорт, логистика', lat=55.52063, lng=37.774149),
 Point(profarea='Административный персонал', lat=48.745092, lng=44.499916),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=55.778796, lng=37.598825),
 Point(profarea='Продажи', lat=57.001064, lng=40.968217),
 Point(profarea='Бухгалтерия, управленческий учет, финансы предприятия', lat=50.576507, lng=36.578904),
 Point(profarea='Строительство, недвижимость', lat=54.169395, lng=37.58279),
 Point(profarea='Наука, образование', lat=55.789633, lng=49.134055),
 Point(profarea='Домашний персонал', lat=55.789633, lng=49.134055),
 Point(profarea='Продажи', lat=59.947991, lng=30.268051),
 Point(profarea='Информационные технологии, интернет, телеком', lat=59.947991, lng=30.268051),
 Point(profarea='Транспорт, логистика', lat=56.358208, lng=37.505678),
 Point(profarea='Рабочий персонал', lat=56.358208, lng=37.505678),
 Point(profarea='Административный персонал', lat=56.836101, lng=60.614578),
 Point(profarea='Продажи', lat=56.836101, lng=60.614578),
 Point(profarea='Продажи', lat=55.689695, lng=37.467482),
 Point(profarea='Продажи', lat=55.8623392366, lng=37.5676616141),
 Point(profarea='Строительство, недвижимость', lat=44.740913, lng=37.724526),
 Point(profarea='Медицина, фармацевтика', lat=53.499701, lng=49.275127),
 Point(profarea='Продажи', lat=44.740913, lng=37.724526),
 Point(profarea='Административный персонал', lat=55.558208, lng=37.695501),
 Point(profarea='Продажи', lat=55.558208, lng=37.695501),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=55.558208, lng=37.695501),
 Point(profarea='Искусство, развлечения, масс-медиа', lat=59.9410372053, lng=30.2767724527),
 Point(profarea='Маркетинг, реклама, PR', lat=59.9410372053, lng=30.2767724527),
 Point(profarea='Туризм, гостиницы, рестораны', lat=59.935638, lng=30.361673),
 Point(profarea='Продажи', lat=55.039068, lng=82.971696),
 Point(profarea='Продажи', lat=55.890239, lng=37.422548),
 Point(profarea='Административный персонал', lat=54.903112, lng=52.304937),
 Point(profarea='Продажи', lat=54.903112, lng=52.304937),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=54.903112, lng=52.304937),
 Point(profarea='Медицина, фармацевтика', lat=55.439242, lng=37.56635),
 Point(profarea='Управление персоналом, тренинги', lat=53.184521, lng=50.104092),
 Point(profarea='Медицина, фармацевтика', lat=55.439242, lng=37.56635),
 Point(profarea='Транспорт, логистика', lat=55.750358, lng=37.755895),
 Point(profarea='Строительство, недвижимость', lat=51.686534, lng=39.256746),
 Point(profarea='Продажи', lat=51.686534, lng=39.256746),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=55.856341, lng=37.493847),
 Point(profarea='Туризм, гостиницы, рестораны', lat=59.928438, lng=30.35887),
 Point(profarea='Административный персонал', lat=59.928438, lng=30.35887),
 Point(profarea='Туризм, гостиницы, рестораны', lat=59.7999429934, lng=30.271653709),
 Point(profarea='Автомобильный бизнес', lat=56.129504, lng=47.300037),
 Point(profarea='Медицина, фармацевтика', lat=54.715909, lng=20.484795),
 Point(profarea='Начало карьеры, студенты', lat=53.899117, lng=27.524919),
 Point(profarea='Автомобильный бизнес', lat=53.899117, lng=27.524919),
 Point(profarea='Транспорт, логистика', lat=55.825391, lng=37.353971),
 Point(profarea='Начало карьеры, студенты', lat=47.21933, lng=39.713827),
 Point(profarea='Продажи', lat=47.21933, lng=39.713827),
 Point(profarea='Строительство, недвижимость', lat=45.014256, lng=38.956242),
 Point(profarea='Производство, сельское хозяйство', lat=45.014256, lng=38.956242),
 Point(profarea='Продажи', lat=55.531188, lng=37.620465),
 Point(profarea='Инсталляция и сервис', lat=56.079904, lng=93.011349),
 Point(profarea='Производство, сельское хозяйство', lat=56.079904, lng=93.011349),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=55.829593, lng=37.371164),
 Point(profarea='Транспорт, логистика', lat=53.899117, lng=27.524919),
 Point(profarea='Административный персонал', lat=56.830822, lng=60.597501),
 Point(profarea='Информационные технологии, интернет, телеком', lat=55.785979, lng=37.660521),
 Point(profarea='Искусство, развлечения, масс-медиа', lat=55.785979, lng=37.660521),
 Point(profarea='Маркетинг, реклама, PR', lat=55.785979, lng=37.660521),
 Point(profarea='Продажи', lat=50.59399, lng=36.600563),
 Point(profarea='Административный персонал', lat=61.643647, lng=50.823454),
 Point(profarea='Туризм, гостиницы, рестораны', lat=45.074572, lng=41.941191),
 Point(profarea='Административный персонал', lat=45.074572, lng=41.941191),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=56.881502, lng=60.51041),
 Point(profarea='Административный персонал', lat=59.856829603, lng=30.315072455),
 Point(profarea='Начало карьеры, студенты', lat=60.0089569106, lng=30.2589382952),
 Point(profarea='Туризм, гостиницы, рестораны', lat=60.0089569106, lng=30.2589382952),
 Point(profarea='Домашний персонал', lat=55.659231, lng=37.560835),
 Point(profarea='Административный персонал', lat=56.331972, lng=36.733432),
 Point(profarea='Медицина, фармацевтика', lat=56.331972, lng=36.733432),
 Point(profarea='Производство, сельское хозяйство', lat=55.873203, lng=37.122619),
 Point(profarea='Транспорт, логистика', lat=55.631773, lng=37.618031),
 Point(profarea='Безопасность', lat=55.775388, lng=37.657817),
 Point(profarea='Юристы', lat=55.853718, lng=48.568126),
 Point(profarea='Медицина, фармацевтика', lat=59.957782, lng=30.342189),
 Point(profarea='Административный персонал', lat=51.535389, lng=46.01815),
 Point(profarea='Продажи', lat=51.535389, lng=46.01815),
 Point(profarea='Спортивные клубы, фитнес, салоны красоты', lat=51.535389, lng=46.01815),
 Point(profarea='Транспорт, логистика', lat=55.705645, lng=37.689447),
 Point(profarea='Рабочий персонал', lat=55.705645, lng=37.689447),
 Point(profarea='Транспорт, логистика', lat=55.794067, lng=49.169098),
 Point(profarea='Административный персонал', lat=43.673405, lng=40.297525),
 Point(profarea='Административный персонал', lat=44.9984885793, lng=39.0794369012),
 Point(profarea='Производство, сельское хозяйство', lat=55.726396, lng=37.381801),
 Point(profarea='Рабочий персонал', lat=55.726396, lng=37.381801),
 Point(profarea='Туризм, гостиницы, рестораны', lat=53.348763, lng=83.777009),
 Point(profarea='Начало карьеры, студенты', lat=47.270275, lng=39.72617),
 Point(profarea='Продажи', lat=47.270275, lng=39.72617),
 Point(profarea='Юристы', lat=55.775545, lng=37.586077),
 Point(profarea='Производство, сельское хозяйство', lat=55.853895, lng=37.517734),
 Point(profarea='Административный персонал', lat=47.236788, lng=39.614797),
 Point(profarea='Банки, инвестиции, лизинг', lat=55.863328, lng=37.540093),
 Point(profarea='Бухгалтерия, управленческий учет, финансы предприятия', lat=55.863328, lng=37.540093),
 Point(profarea='Рабочий персонал', lat=55.879101, lng=38.479811),
 Point(profarea='Бухгалтерия, управленческий учет, финансы предприятия', lat=55.73441, lng=37.59047),
 Point(profarea='Строительство, недвижимость', lat=59.910641, lng=30.267404),
 Point(profarea='Начало карьеры, студенты', lat=55.719536, lng=37.628155),
 Point(profarea='Туризм, гостиницы, рестораны', lat=55.719536, lng=37.628155),
 Point(profarea='Маркетинг, реклама, PR', lat=54.953918, lng=82.878056)]
In [49]:
areas = reduce(lambda l, p: l + [Area(p.profarea, [p])] if not any(list(map(partial(Area.__iand__, other=p), l))) else l, points, [])
In [50]:
geo_profareas_grouped_df = pd.DataFrame(zip(
    map(attrgetter('profarea'), areas),
    map(attrgetter('count'), areas),
    map(attrgetter('center_lat'), areas),
    map(attrgetter('center_lng'), areas),
), columns=['profarea', 'count', 'center_lat', 'center_lng'])
In [51]:
geo_profareas_grouped_df.head(10)
Out[51]:
profarea count center_lat center_lng
0 Административный персонал 229 59.904215 30.321857
1 Домашний персонал 32 59.981839 30.317089
2 Транспорт, логистика 887 55.724103 37.582520
3 Административный персонал 23 48.348426 44.214197
4 Спортивные клубы, фитнес, салоны красоты 337 55.717553 37.631853
5 Продажи 2771 55.773011 37.838572
6 Бухгалтерия, управленческий учет, финансы пред... 25 51.704838 37.703753
7 Строительство, недвижимость 40 52.520757 39.150968
8 Наука, образование 11 55.558580 49.238237
9 Домашний персонал 8 54.450943 49.326572
In [62]:
len(areas)
Out[62]:
1848
In [52]:
px.scatter_geo(
    geo_profareas_grouped_df,
    lat='center_lat',
    lon='center_lng',
    size='count',
    fitbounds='locations',
    color='profarea',
    center={'lat': 53, 'lon': 83},
    size_max=50,
    labels={'count': 'Количество вакансий'},
    title='Количество вакансий и средняя зарплата относительно города'
)
In [53]:
all_profareas = reduce(set.union, specs_df.specialization_profarea_names, set())
In [54]:
len(all_profareas)
Out[54]:
28
In [55]:
{profarea: specs_df[specs_df.specialization_profarea_names.map({profarea}.issubset)]['mean_salary'].mean() for profarea
 in all_profareas}
Out[55]:
{'Высший менеджмент': 123710.24584717608,
 'Закупки': 62442.23834886817,
 'Управление персоналом, тренинги': 65502.94584382872,
 'Инсталляция и сервис': 61575.194444444445,
 'Спортивные клубы, фитнес, салоны красоты': 51376.46395250212,
 'Консультирование': 92617.73353751915,
 'Безопасность': 53167.152825836216,
 'Домашний персонал': 41872.405241935485,
 'Начало карьеры, студенты': 43474.821203244705,
 'Производство, сельское хозяйство': 64457.76760048721,
 'Добыча сырья': 94832.55590062111,
 'Бухгалтерия, управленческий учет, финансы предприятия': 48283.09805153991,
 'Строительство, недвижимость': 84771.85741049125,
 'Автомобильный бизнес': 72177.20721925133,
 'Искусство, развлечения, масс-медиа': 56821.15873015873,
 'Туризм, гостиницы, рестораны': 47756.217606707316,
 'Наука, образование': 45231.578881987574,
 'Страхование': 72418.31764705882,
 'Рабочий персонал': 64773.36851851852,
 'Маркетинг, реклама, PR': 60350.047784967646,
 'Продажи': 49176.52389049481,
 'Государственная служба, некоммерческие организации': 52380.44400785855,
 'Юристы': 55641.35897435898,
 'Банки, инвестиции, лизинг': 57242.99230111206,
 'Административный персонал': 48667.45219370861,
 'Информационные технологии, интернет, телеком': 92008.00538176925,
 'Транспорт, логистика': 67427.33646295663,
 'Медицина, фармацевтика': 55659.44958753437}
In [56]:
df_profarea = pd.DataFrame({
    profarea: specs_df[
        specs_df.specialization_profarea_names
            .map({profarea}.issubset)
    ]['mean_salary'].agg(['count', 'sum', 'mean', 'max', 'min'])
    for profarea in all_profareas
}).T
df_profarea = df_profarea.astype(np.int64).reset_index().rename(columns={'index': 'profarea'})
In [57]:
df_profarea.head(10)
Out[57]:
profarea count sum mean max min
0 Высший менеджмент 602 74473568 123710 1200000 150
1 Закупки 751 46894121 62442 300000 1
2 Управление персоналом, тренинги 794 52009339 65502 525000 10000
3 Инсталляция и сервис 468 28817191 61575 200000 1300
4 Спортивные клубы, фитнес, салоны красоты 1179 60572851 51376 1001000 3000
5 Консультирование 653 60479380 92617 1650000 12500
6 Безопасность 1734 92191843 53167 338696 1500
7 Домашний персонал 496 20768713 41872 250000 1300
8 Начало карьеры, студенты 8876 385882513 43474 550000 30
9 Производство, сельское хозяйство 4105 264599136 64457 650000 1
In [58]:
px.bar(
    df_profarea,
    x='profarea',
    y='count',
    labels={'profarea': 'Профобласть', 'count': 'Количество вакансий'},
    text_auto='.2s',
    title='Количество вакансий в каждой области'
).update_xaxes(categoryorder='total descending')
In [59]:
px.pie(
    df_profarea,
    names='profarea',
    values='count',
    labels={'index': 'Профобласть', 'count': 'Количество вакансий'},
    title='Доля вакансий для каждой области'
)
In [60]:
px.bar(
    df_profarea,
    x='profarea',
    y='mean',
    labels={'profarea': 'Профобласть', 'mean': 'Средняя зарплата'},
    text_auto='.2s',
    title='Средняя зарплата в каждой области'
).update_xaxes(categoryorder='total descending')
In [61]:
px.bar(
    df_profarea,
    x='profarea',
    y='sum',
    labels={'profarea': 'Профобласть', 'sum': 'Сумма всех зарплат'},
    text_auto='.2s',
    title='Сумма зарплат в каждой области'
).update_xaxes(categoryorder='total descending')